package hikari.script;

import com.gooddies.wiring.Wiring;
import com.gooddies.wiring.annotations.PostWiring;
import com.gooddies.wiring.annotations.Wire;
import com.gooddies.wiring.annotations.WiringComponent;
import hikari.preferences.GameProperties;
import hikari.script.compiler.VS8Compiler;
import hikari.utils.FileContentReplacer;
import hikari.utils.Utils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;
import org.xml.sax.SAXException;

/**
 * @author Dmitry
 */
@WiringComponent
public class ScriptRegisterProcessor {

    private String mediaFolder;
    @Wire
    private GameProperties gameProperties;
    @Wire
    private Utils utils;
    @Wire
    private FileContentReplacer fileContentReplacer;

    private ScriptRegisterProcessor() {

    }

    @PostWiring
    private void init() {
        mediaFolder = gameProperties.getMediaFolder();
    }

    public void process() {
        try {
            List<ScriptFile> scripts = getListOfFiles(mediaFolder);
            VS8Compiler compiler = Wiring.getComponent(VS8Compiler.class);
            compiler.setBaseFolder(gameProperties.getCppProjectFolder());
            compiler.synchronizeProjectWithListOfScript(scripts);
            registerScriptsInGameSources(scripts);
        } catch (TransformerConfigurationException ex) {
            utils.logError(ex);
        } catch (TransformerException ex) {
            utils.logError(ex);
        } catch (ParserConfigurationException | SAXException | IOException | XPathExpressionException ex) {
            utils.logError(ex);
        }
    }

    protected void registerScriptsInGameSources(List<ScriptFile> scripts) {
        String body = generateCppSwitch(scripts);
        String pathToCppProjectFolder = utils.combinePaths(gameProperties.getCppProjectFolder(), "project");
        String pathToScriptManagerCpp = utils.combinePaths(pathToCppProjectFolder, "ScriptManager.cpp");
        fileContentReplacer.replaceTextInFile("SCRIPT_REGISTER_START", "SCRIPT_REGISTER_END", pathToScriptManagerCpp, System.lineSeparator() + body);

        String includes = generateIncludesForScript(scripts);
        String pathToScriptHeadersH = utils.combinePaths(pathToCppProjectFolder, "scriptHeaders.h");
        fileContentReplacer.replaceTextInFile("SCRIPT_INCLUDES_START", "SCRIPT_INCLUDES_END", pathToScriptHeadersH, System.lineSeparator() + includes);
    }

    private String generateIncludesForScript(List<ScriptFile> scripts) {
        StringBuilder sb = new StringBuilder(1000);
        for (ScriptFile file : scripts) {
            String include = String.format("#include \"../../media/%s\"", file.getPath().replace('\\', '/'));
            sb.append(include);
            newLine(sb);
        }
        return sb.toString();
    }

    private void newLine(StringBuilder sb) {
        sb.append(System.lineSeparator());
    }

    private String generateCppSwitch(List<ScriptFile> scripts) {
        StringBuilder sb = new StringBuilder();
        newLine(sb);
        sb.append("\tlong hash=Utils::hash_WString(className);");
        newLine(sb);
        sb.append("\tswitch(hash){");
        newLine(sb);
        for (ScriptFile file : scripts) {
            generateSwitchCase(sb, file);
        }

        newLine(sb);
        sb.append("\t}");
        newLine(sb);
        sb.append("\tprintf(\"Error: cannot instantiate class %s\\\\n\", className.c_str());");
        newLine(sb);
        sb.append("\tthrow \"Error: cannot instantiate class\";");
        newLine(sb);
        sb.append("\treturn NULL;");
        newLine(sb);
        sb.append("\t");
        return sb.toString();
    }

    private void generateSwitchCase(StringBuilder sb, ScriptFile script) {
        sb.append("\t\tcase ").append(script.getPath().hashCode()).append(": {//").append(script.getPath());
        newLine(sb);
        sb.append("\t\t\treturn new ").append(script.getClassName()).append("();");
        newLine(sb);
        sb.append("\t\t}");
        newLine(sb);

    }

    protected List<ScriptFile> getListOfFiles(String baseFolder) {
        ArrayList<ScriptFile> list = new ArrayList<>(20);
        File dir = new File(baseFolder);
        File[] files = dir.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                if (!file.getName().startsWith(".")) {//hidden unix folders. ie ".svn"
                    List<ScriptFile> listOfFiles = getListOfFiles(utils.combinePaths(baseFolder, file.getName()));
                    list.addAll(listOfFiles);
                }
            } else {
                String fileName = file.getName().toLowerCase();

                if (fileName.endsWith(".h") || fileName.endsWith(".hpp")) {
                    ScriptFile script = tryParseFile(file);
                    if (script != null) {
                        list.add(script);
                    }
                }
            }
        }

        return list;
    }

    private ScriptFile tryParseFile(File file) {
        ScriptClassNameExtractor extractor = Wiring.getComponent(ScriptClassNameExtractor.class);
        String className = extractor.extractClassName(file.getAbsolutePath());
        if (className == null) {
            return null;
        }

        ScriptFile script = new ScriptFile();
        script.setClassName(className);
        script.setPath(utils.makePathRelative(file.getAbsolutePath(), mediaFolder));

        String cppPath = utils.formatPathWithNewExtension(file.getAbsolutePath(), ".cpp");
        if (new File(cppPath).exists()) {
            script.setLinkedCPPFile(utils.makePathRelative(cppPath, mediaFolder));
        }

        return script;
    }
}
